package net.embedded_projects.octopus.usb;

import java.io.IOException;

import net.embedded_projects.octopus.Octopus;
import ch.ntb.usb.Device;
import ch.ntb.usb.USB;
import ch.ntb.usb.USBException;

public class OctopusUSB implements Octopus {

	public final static short VID = 0x1781;
	public final static short PID = 0x0c65;
	private static final int USB_TIMEOUT = 100;
	private static final int BULK_WRITE_ADDR = 0x01;
	private static final int BULK_READ_ADDR = 0x81;

	private Device usbDevice;
	
	private GPIO gpio;
	
	public ADC adc;
	
	private I2C i2c;
	
	public class GPIO implements Octopus.GPIO {
		
		@Override
		public void initPort(Octopus.GPIO.Port port) throws IOException {
			message(new byte[]{CMD_IO_INIT_PORT, 0x01, (byte) port.ordinal()}, 2);
		}
		
		@Override
		public void initPin(int pin) throws IOException {
			message(new byte[]{CMD_IO_INIT_PIN, 0x01, (byte) pin}, 2);
		}

		@Override
		public boolean getPin(int pin) throws IOException {
			byte[] result = message(new byte[]{CMD_IO_PIN_GET, 0x01, (byte) pin}, 3);
			return result[2] != 0;
		}

		@Override
		public byte getPort(Port port) throws IOException {
			byte[] result = message(new byte[]{CMD_IO_PORT_GET, 0x01, (byte) port.ordinal()}, 4);
			return result[2];
		}

		@Override
		public void setPin(int pin, boolean value) throws IOException {
			message(new byte[]{CMD_IO_PIN_SET, 0x02, (byte) pin, (byte) (value?1:0)}, 2);
		}

		@Override
		public void setPinDirectionIn(int pin) throws IOException {
			message(new byte[]{CMD_IO_PIN_DIRECTION_IN, 0x01, (byte) pin}, 2);
		}

		@Override
		public void setPinDirectionOut(int pin) throws IOException {
			message(new byte[]{CMD_IO_PIN_DIRECTION_OUT, 0x01, (byte) pin}, 2);
		}

		@Override
		public void setPinDirectionTri(int pin) throws IOException {
			message(new byte[]{CMD_IO_PIN_DIRECTION_TRI, 0x01, (byte) pin}, 2);
		}

		@Override
		public void setPort(Port port, byte value) throws IOException {
			message(new byte[]{CMD_IO_PORT_SET, 0x02, (byte) port.ordinal(), value}, 2);
		}

		@Override
		public void setPortDirectionIn(Port port, byte mask) throws IOException {
			message(new byte[]{CMD_IO_PORT_DIRECTION_IN, 0x02, (byte) port.ordinal(), mask}, 2);
		}

		@Override
		public void setPortDirectionOut(Port port, byte mask) throws IOException {
			message(new byte[]{CMD_IO_PORT_DIRECTION_OUT, 0x02, (byte) port.ordinal(), mask}, 2);
		}

		@Override
		public void setPortDirectionTri(Port port, byte mask) throws IOException {
			message(new byte[]{CMD_IO_PORT_DIRECTION_OUT, 0x02, (byte) port.ordinal(), mask}, 2);
		}
		
	}
	
	public class ADC implements Octopus.ADC {
		
		public void init(int pin) throws IOException {
			message(new byte[]{CMD_ADC_INIT_PIN, 0x01, (byte) (pin+33)}, 2);
		}
		
		@Override
		public int get(int pin) throws IOException {
			byte[] result = message(new byte[]{CMD_ADC_GET, 0x01, (byte) (pin+33)}, 4);
			return result[2]<<8 | result[3];			
		}

		@Override
		public int ref(int ref) throws IOException {
			byte[] result = message(new byte[]{CMD_ADC_REF, 0x01, (byte) ref}, 3);
			return result[2];
		}
	}
	
	public class I2C implements Octopus.I2C {
		
		public I2C() {
			try {
				message(new byte[]{CMD_I2C_INIT, 0x00, 0x00}, 2);
			} catch (IOException e) {
			}
		}
		
		public int setBitrate(int speed) throws IOException {
			byte[] msg = {CMD_I2C_BITRATE, 0x01, (byte) speed};
			byte[] result = message(msg, 2);
			if(result.length == 2)
				return result[2];
			else
				return 1;
		}
		
		public void sendStart() throws IOException {
			byte[] msg = {CMD_I2C_SEND_START, 0x00, 0x00};
			message(msg, 2);
		}
		
		public void sendStop() throws IOException {
			byte[] msg = {CMD_I2C_SEND_STOP, 0x00, 0x00};
			message(msg, 2);
		}
		
		public int sendBytes(byte[] buf, int timeout) throws Exception {
			if(buf.length > 60)
				throw new Exception("buf to long (max 60 bytes)");
			
			byte[] msg = new byte[buf.length + 4];
			msg[0] = CMD_I2C_SEND_BYTES;
			msg[1] = (byte) (buf.length + 4);
			msg[2] = (byte) timeout;
			for(int i=0; i<buf.length; i++)
				msg[i+4] = buf[i];
			
			byte[] result = message(msg, 2);
			if(result.length == 2)
				return result[2];
			else
				return 1;
		}
		
		public byte[] receiveBytes(int address, int len, int timeout) throws Exception {
			if(len > 60)
				throw new Exception("buf to long (max 60 bytes)");
			
			byte[] msg = {CMD_I2C_RECV_BYTES, (byte) (len+4), (byte) timeout, (byte) address};
			
			byte[] result = message(msg, 2);
			return result;
		}
		
		public void dispose() {
			try {
				message(new byte[]{CMD_I2C_DEINIT,0x00,0x00}, 2);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	public OctopusUSB() {
	}
	
	public void open() throws IOException {
		usbDevice = USB.getDevice(VID, PID);
		usbDevice.updateDescriptors();
		if(usbDevice.getConfigDescriptors() == null)
			throw new IOException("Device not found!");
		usbDevice.open(usbDevice.getConfigDescriptors()[0].getBConfigurationValue(), 0, 0);
		getHWDescription();
		getADC().ref(PARAM_ADC_AVCC);
		getADC().init(1);
		System.out.println("ADC: " + adc.get(1));
	}
	
	public void close() throws IOException {
		usbDevice.close();
	}
	
	public boolean isOpen() {
		return (usbDevice != null && usbDevice.isOpen());
	}
	
	public void reset() {
		// TODO Auto-generated method stub		
	}
	
	@Override
	public void getHWDescription() throws IOException {
		byte[] result = message(new byte[]{CMD_GET_HW_ID, 0x00, 0x00}, 13);
		System.out.println("Octopus HW desc: " + result);
	}
	
	private byte[] message(byte[] data, int answerLen) throws IOException {
		if(usbDevice.writeBulk(BULK_WRITE_ADDR, data, data.length, USB_TIMEOUT, true) < data.length)
			throw new USBException("Transfer error occurred!");
		
		byte[] answer = new byte[answerLen];
		if(answerLen > 0)
				try {
					usbDevice.readBulk(BULK_READ_ADDR, answer, answerLen, USB_TIMEOUT, true);
					return answer;
				} catch(Exception e) {}
		
		return answer;
	}
	
	@Override
	public GPIO getGPIO() {
		if(gpio == null) gpio = new GPIO();
		return gpio;
	}
	
	public ADC getADC() {
		if(adc == null) adc = new ADC();
		return adc;
	}
	
	public I2C getI2C() {
		if(i2c == null) i2c = new I2C();
		return i2c;
	}
}
